#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/prctl.h>
#include <sys/socket.h>

#include "client.h"

static int renderEventHandler( Tcl_Event* evPtr, int flags ){
	
	int i;
	const char* res;
	unsigned char pos;
	char type;
	const char* buf[200];
	char statusText[1024];
	struct renderEvent* render;
	struct timeval startTime;
	struct timeval endTime;
	int avgRender;
	
	//get start time
	gettimeofday( &startTime, NULL );
	
	//init some vars
	render = ( struct renderEvent* )evPtr;
	for( i = 0; i < 200; i++ ) buf[i] = "0";
	
	//render objects to buf
	for( i = 0; i < render->count * 2; i++ ){
		
		type = render->data[i];
		i++;
		pos = render->data[i];
		
		if( type == 'f' ){
			if( pos > 199 ) continue;
			buf[pos] = "1";
		}
		
		if( pos < 11 || pos > 188 ) continue;
		
		if( type == 'u' || type == 'd' ){
			buf[ pos - 1 ] = "1";
			buf[ pos     ] = "1";
			buf[ pos + 1 ] = "1";
			if( type == 'u' ){
				buf[ pos - 10 ] = "1";
				buf[ pos + 9  ] = "1";
				buf[ pos + 11 ] = "1";
			} else {
				buf[ pos - 11 ] = "1";
				buf[ pos - 9  ] = "1";
				buf[ pos + 10 ] = "1";
			}
		}else if( type == 'l' || type == 'r' ){
			buf[ pos - 10 ] = "1";
			buf[ pos      ] = "1";
			buf[ pos + 10 ] = "1";
			if( type == 'l' ){
				buf[ pos - 9  ] = "1";
				buf[ pos - 1  ] = "1";
				buf[ pos + 11 ] = "1";
			} else {
				buf[ pos - 11 ] = "1";
				buf[ pos + 1  ] = "1";
				buf[ pos + 9  ] = "1";
			}
		}else if( type == 'b' ){
			buf[ pos - 10 ] = "1";
			buf[ pos - 1  ] = "1";
			buf[ pos + 1  ] = "1";
			buf[ pos + 10 ] = "1";
		}else if( type == 'B' ){
			buf[ pos      ] = "1";
			buf[ pos - 11 ] = "1";
			buf[ pos - 9  ] = "1";
			buf[ pos + 9  ] = "1";
			buf[ pos + 11 ] = "1";
		}
	}
	
	if( render->count ) ckfree( render->data );
	
	//"render" buffer on checkboxes
	for( i = 0; i < 200; i++ ){
		res = Tcl_SetVar( render->tcl, fieldPixelsIds[i], buf[i], TCL_GLOBAL_ONLY );
		if( !res ){
			printf( "on err\n" );
			break;
		}
	}
	
	//calculate render time
	gettimeofday( &endTime, NULL );
	render->avgRender[*render->avgIndex] = (endTime.tv_usec+((endTime.tv_sec-startTime.tv_sec)*1000000)) - startTime.tv_usec;
	(*render->avgIndex)++;
	if( *render->avgIndex == 10 ) {
		*render->avgIndex = 0;
		
		avgRender = 0;
		
		for( i = 0; i < 10; i++ ) avgRender += render->avgRender[i];
		
		avgRender /= 10;
		
		//update status bar
		sprintf( statusText, "RT: %ius", avgRender );
		Tcl_SetVar( render->tcl, "statusText", statusText, 0 );
	}
	
	return 1;
}

static int allertEventHandler( Tcl_Event* evPtr, int flags ){
	
	struct alertEvent* ev;
	char buf[4096];
	
	ev = (struct alertEvent*)evPtr;
	
	sprintf( buf, "tk_messageBox -type ok -icon info -title Message -message \"%s\"", ev->msg );
	Tcl_Eval( ev->tcl, buf );
	
	//ckfree( ev->msg );
	
	return 1;
}

static int connect2serverResult( Tcl_Event* evPtr, int flags ){
	
	struct alertEvent* ev;
	char buf[4096];
	
	ev = (struct alertEvent*)evPtr;
	sprintf( buf, "connect2serverRes \"%s\"", ev->msg );
	Tcl_Eval( ev->tcl, buf );
	//ckfree( ev->msg );
	return 1;
}


/*
net proto
server -> client
<num objs u8>    <obj desc u8> <obj pos u8> ....

client -> server
<intent u8> <intent desc u8>
*/


static void networkThreadCleanup( void *arg ) {
	
	struct instance* this;
	
	this = arg;
	
	close( this->netClientSocket );
	this->netClientSocket = -1;
}

static void* networkThread( void* arg ){
	
	int res;
	int bytes;
	char buf[16];
	char* msg2gui;
	struct instance* this;
	struct renderEvent* ev;
	struct alertEvent* dropEv;
	
	pthread_cleanup_push( networkThreadCleanup, arg );
	prctl( PR_SET_NAME, "net", 0, 0, 0 );
	
	this = arg;
	msg2gui = "Failed to connect to server.";
	
	//connect to server
	res = connect( this->netClientSocket, (struct sockaddr*)&this->serverAddress, sizeof(this->serverAddress) );
	if( res ){
		perror( "connect" );
		goto l_connectResult;
	}
	
	//handshake
	res = write( this->netClientSocket, "TANKS", 5 );
	if( res != 5 ) goto l_connectResult;
	
	res = recv( this->netClientSocket, buf, 2, MSG_WAITALL );
	if( res != 2 ) goto l_connectResult;
	
	buf[2] = 0x00;
	if( strcmp(buf, "OK") ) goto l_connectResult;
	
	msg2gui = "0";
	
l_connectResult:
	
	dropEv = ckalloc( sizeof(*dropEv) );
	dropEv->tclEvent.proc = connect2serverResult;
	dropEv->tcl = this->tcl;
	dropEv->msg = msg2gui;
	
	Tcl_ThreadQueueEvent( this->tclThread, (Tcl_Event*)dropEv, TCL_QUEUE_TAIL );
	Tcl_ThreadAlert( this->tclThread );
	
	if( msg2gui[0] != '0' ) goto l_exit;
	
l_again:
	
	//obj count
	res = recv( this->netClientSocket, buf, 1, MSG_WAITALL );
	if( res != 1 ) goto l_drop;
	
	ev = ckalloc( sizeof(*ev) );
	ev->tclEvent.proc = renderEventHandler;
	ev->tcl = this->tcl;
	ev->count = buf[0];
	ev->avgRender = this->avgRender;
	ev->avgIndex  = &this->avgIndex;
	
	if( buf[0] ) {
		bytes = buf[0] * 2;
		ev->data = ckalloc( bytes );
		res = recv( this->netClientSocket, ev->data, bytes, MSG_WAITALL );
		if( res != bytes ){
			ckfree( ev->data );
			ckfree( ev );
			goto l_drop;
		}
	}
	
	Tcl_ThreadQueueEvent( this->tclThread, (Tcl_Event*)ev, TCL_QUEUE_TAIL );
	Tcl_ThreadAlert( this->tclThread );
	
	goto l_again;
	
l_drop:
	dropEv = ckalloc( sizeof(*dropEv) );
	dropEv->tclEvent.proc = allertEventHandler;
	dropEv->tcl = this->tcl;
	dropEv->msg = "Connection lost.";
	
	Tcl_ThreadQueueEvent( this->tclThread, (Tcl_Event*)dropEv, TCL_QUEUE_TAIL );
	Tcl_ThreadAlert( this->tclThread );
	
l_exit:
	pthread_cleanup_pop( 1 );
	return NULL;
}

static int benchCore( ClientData clientData, Tcl_Interp* tcl, int objc, Tcl_Obj* const objv[] ){
	
	int i;
	char* val;
	const char* res;
	
	if( objc < 2 ) return TCL_OK;
	val = Tcl_GetString( objv[1] );
	if( !val ) return TCL_OK;
	
	for( i = 0; i < 200; i++ ){
		res = Tcl_SetVar( tcl, fieldPixelsIds[i], val, TCL_GLOBAL_ONLY );
		if( !res ) return TCL_ERROR;
	}
	
	return TCL_OK;
}

static int connect2serverCore( ClientData clientData, Tcl_Interp* tcl, int objc, Tcl_Obj* const objv[] ){
	
	int res;
	const char* ipStr;
	const char* portStr;
	unsigned short port;
	struct instance* this;
	struct timeval socketTimeout;
	
	this = clientData;
	socketTimeout.tv_sec  = 10;
	socketTimeout.tv_usec = 0;
	
	//check already connected
	if( this->netClientSocket >= 0 ){
		Tcl_SetVar( tcl, "resmsg", "Already connected.", 0 );
		return TCL_OK;
	}
	
	//get string ip and port values from gui
	ipStr   = Tcl_GetVar( tcl, "serverIp",   TCL_GLOBAL_ONLY );
	portStr = Tcl_GetVar( tcl, "serverPort", TCL_GLOBAL_ONLY );
	
	//check ip not null
	if( !ipStr ){
		Tcl_SetVar( tcl, "resmsg", "Enter server IP.", 0 );
		return TCL_OK;
	}
	
	//check port not null
	if( !portStr ){
		Tcl_SetVar( tcl, "resmsg", "Enter server port.", 0 );
		return TCL_OK;
	}
	
	//set net family
	this->serverAddress.sin_family = AF_INET;
	
	//parse and check ip
	this->serverAddress.sin_addr.s_addr = inet_addr( ipStr );
	if( this->serverAddress.sin_addr.s_addr == -1 ){
		Tcl_SetVar( tcl, "resmsg", "Wrong server IP.", 0 );
		return TCL_OK;
	}
	
	//parse and check port
	res = sscanf( portStr, "%hu", &port );
	if( res != 1 ){
		Tcl_SetVar( tcl, "resmsg", "Wrong server port.", 0 );
		return TCL_OK;
	}
	this->serverAddress.sin_port = htons( port );
	
	//create socket
	this->netClientSocket = socket( AF_INET, SOCK_STREAM, 0 );
	if( this->netClientSocket < 0 ){
		perror( "socket" );
		Tcl_SetVar( tcl, "resmsg", "Create socket failed.", 0 );
		return TCL_OK;
	}
	
	//set SO_RCVTIMEO for socket
	res = setsockopt( this->netClientSocket, SOL_SOCKET, SO_RCVTIMEO, &socketTimeout, sizeof(socketTimeout) );
	if( res ){
		perror( "socket set SO_RCVTIMEO" );
		Tcl_SetVar( tcl, "resmsg", "Failed to configure socket.", 0 );
		close( this->netClientSocket );
		this->netClientSocket = -1;
		return TCL_OK;
	}
	
	//set SO_SNDTIMEO for socket
	res = setsockopt( this->netClientSocket, SOL_SOCKET, SO_SNDTIMEO, &socketTimeout, sizeof(socketTimeout) );
	if( res ){
		perror( "socket set SO_SNDTIMEO" );
		Tcl_SetVar( tcl, "resmsg", "Failed to configure socket.", 0 );
		close( this->netClientSocket );
		this->netClientSocket = -1;
		return TCL_OK;
	}
	
	//start network thread
	res = pthread_create( &this->netThread, NULL, networkThread, this );
	if( res ){
		printf( "net pthread_create: %s\n", strerror(res) );
		Tcl_SetVar( tcl, "resmsg", "Failed to start network thread.", 0 );
		close( this->netClientSocket );
		this->netClientSocket = -1;
		return TCL_OK;
	}
	
	return TCL_OK;
}

static int dropConnectionCore( ClientData clientData, Tcl_Interp* tcl, int objc, Tcl_Obj* const objv[] ){
	
	struct instance* this;
	
	this = clientData;
	
	if( this->netClientSocket < 0 ) return TCL_OK;
	
	pthread_cancel( this->netThread );
	pthread_join(   this->netThread, NULL );
	
	return TCL_OK;
}

static int tankCmdCore( ClientData clientData, Tcl_Interp* tcl, int objc, Tcl_Obj* const objv[] ){
	
	int res;
	struct instance* this;
	char* cmd;
	char* arg;
	char pendingCmd[2];
	
	this = clientData;
	
	if( objc < 3 ) return TCL_OK;
	cmd = Tcl_GetString( objv[1] );
	arg = Tcl_GetString( objv[2] );
	if( !cmd || !arg ) return TCL_OK;
	
	pendingCmd[1] = arg[0];
	pendingCmd[0] = cmd[0];
	
	res = write( this->netClientSocket, pendingCmd, 2 );
	if( res != 2 ) perror( "socket cmd write" );
	
	return 0;
}

static int tankControlCore( ClientData clientData, Tcl_Interp* tcl, int objc, Tcl_Obj* const objv[] ){
	
	int res;
	struct instance* this;
	char* key;
	char* pressed;
	
	static char oldkey;
	static char keys[3];
	
	char moveDirect[2];
	
	this = clientData;
	
	//get function args
	if( objc < 3 ) return TCL_OK;
	key     = Tcl_GetString( objv[1] );
	pressed = Tcl_GetString( objv[2] );
	if( !key || !pressed ) return TCL_OK;
	
	//update keys array
	if( pressed[0] == '1' ){
		if( keys[0] != key[0] ){
			keys[2] = keys[1];
			keys[1] = keys[0];
			keys[0] = key[0];
		}
	}else{
		if( keys[0] == key[0] ){
			keys[0] = keys[1];
			keys[1] = keys[2];
			keys[2] = 0x00;
		}else if( keys[1] == key[0] ){
			keys[1] = keys[2];
			keys[2] = 0x00;
		}else if( keys[2] == key[0] ){
			keys[2] = 0x00;
		}
	}
	
	//if current key not changed, then done
	if( keys[0] == oldkey ) return 0;
	
	//update current key
	oldkey = keys[0];
	
	moveDirect[0] = 'g';//means gearbox
	moveDirect[1] = oldkey ? oldkey :  0x00;
	
	res = write( this->netClientSocket, moveDirect, 2 );
	if( res != 2 ) perror( "socket control write" );
	
	return 0;
}

int main ( int argc, char* argv[] ){
	
	int res;
	struct instance this;
	
	prctl( PR_SET_NAME, "main", 0, 0, 0 );
	
	this.tclThread = Tcl_GetCurrentThread();
	this.netClientSocket = -1;
	//this.pendingCmd[0] = 0x00;
	//this.moveDirect[0] = 0x00;
	this.avgIndex      = 0;
	
	Tcl_FindExecutable( argv[0] );
	
	//create and init tcl/tk
	this.tcl = Tcl_CreateInterp();
	Tcl_Init( this.tcl );
	Tk_Init(  this.tcl );
	
	//load main window layout
	res = Tcl_EvalFile( this.tcl, "mainWindow.tcl" );
	if( res ){
		printf( "failed to eval mainWindow.tcl\n" );
		goto l_cleanup;
	}
	
	//setup callbacks for functions in tcl
	Tcl_CreateObjCommand( this.tcl, "bench1_core",        benchCore,          &this, NULL );
	Tcl_CreateObjCommand( this.tcl, "connect2serverCore", connect2serverCore, &this, NULL );
	Tcl_CreateObjCommand( this.tcl, "dropConnection",     dropConnectionCore, &this, NULL );
	Tcl_CreateObjCommand( this.tcl, "tankControl",        tankControlCore,    &this, NULL );
	Tcl_CreateObjCommand( this.tcl, "tankCmd",            tankCmdCore,        &this, NULL );
	
	//mait gui loop
	Tk_MainLoop();
	
l_cleanup:
	
	Tcl_DeleteInterp( this.tcl );
	
	printf( "Exit.\n" );
	return 0;
}
